/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package cz.mzk.lidovky.articles.krameriusAccess;

import cz.mzk.rehan.commons.DatastreamId;
import cz.mzk.kramerius.fedoraaccess.FedoraConnector;
import cz.mzk.lidovky.articles.domain.DayTime;
import cz.mzk.lidovky.articles.domain.Issue;
import cz.mzk.rehan.commons.Dom4jUtils;
import cz.mzk.rehan.commons.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.dom4j.Document;
import org.dom4j.Node;
import org.dom4j.XPath;
import org.joda.time.LocalDate;

/**
 * This class stores maping of issue class to the PID of issue digital object for given volume
 * @author Martin Řehánek (rehan@mzk.cz)
 */
public class VolumeAccessImpl implements VolumeAccess {

    private static final Logger logger = Logger.getLogger(VolumeAccessImpl.class.getName());
    private static final String ISSUE_PID_PREFIX = "info:fedora/";
    private static final String[] FOR_CHILDREN_TITLE_STRINGS = {"dětem", "dětské"};
    private final FedoraConnector connector;
    private final XPath issuesXpath = Dom4jUtils.createXPath("//kramerius:hasItem/@rdf:resource");
    private final XPath issueNumberXpath = Dom4jUtils.createXPath("//mods:part[@type='PeriodicalIssue']/mods:detail[@type='issue']/mods:number");
    private final XPath issueDayTimeXpath = Dom4jUtils.createXPath("//mods:physicalDescription/mods:note/text()");
    private final XPath issueDateXpath = Dom4jUtils.createXPath("//mods:part[@type='PeriodicalIssue']/mods:date");
    private final XPath issueTitleXpath = Dom4jUtils.createXPath("//mods:titleInfo/mods:title");
    private final String volumePid;
    private final Integer volumeNumber;
    //Issue:issuePid
    private final Map<Issue, String> issuePidMap = new HashMap<Issue, String>();
    //IssueNumber:date
    private final Map<Integer, LocalDate> issueNumberDateMap = new HashMap<Integer, LocalDate>();
    //Issue:list of duplicate pids
    private final Map<Issue, List<String>> duplicateIssuePidMap = new HashMap<Issue, List<String>>();
    private final Set<Issue> missingIssues = new HashSet<Issue>();

    public VolumeAccessImpl(String volumePid, Integer volumeNumber, FedoraConnector connector) {
        this.volumePid = volumePid;
        this.volumeNumber = volumeNumber;
        this.connector = connector;
        initIssuePidMap();
    }

    private void initIssuePidMap() {
        List<String> issuePidList = getIssues();
        for (String issuePid : issuePidList) {
            Issue issue = getIssue(issuePid);
            if (issue == null) {
                logger.log(Level.SEVERE, "No issue (number, day time, date) in BIBLIO_MODS stream for issue with pid ''{0}''", issuePid);
            } else {
                addIssuePidMapping(issue, issuePid);
                addIssueDateMapping(Integer.valueOf(issue.getNumber()), issue.getDate());
            }
        }
    }

    private List<String> getIssues() {
        Document relsExtDoc = connector.getDatastreamDissemination(volumePid, DatastreamId.RELS_EXT);
//        System.out.println(volumeNumber + "  " + volumePid);
        List<Node> selectedNodes = issuesXpath.selectNodes(relsExtDoc);
        List<String> issuePidList = new ArrayList<String>();
        for (Node node : selectedNodes) {
            String issuePidPrefixed = node.getText();
            String issuePid = issuePidPrefixed.substring(ISSUE_PID_PREFIX.length());
            issuePidList.add(issuePid);
            //System.out.println("issue pid:" + issuePid);
        }
        return issuePidList;
    }

    private Issue getIssue(String issuePid) {
        Document modsDoc = connector.getDatastreamDissemination(issuePid, DatastreamId.BIBLIO_MODS);
        Integer issueNumber = findIssueNumber(modsDoc, issuePid);
        DayTime dayTime = findIssueDayTime(modsDoc, issuePid);
        LocalDate date = findIssueDate(modsDoc, issuePid);
        boolean forChildren = isForChildren(modsDoc, issuePid);
        int stars = findStars(modsDoc, issuePid);
        return Issue.instanceOf(issueNumber, date, dayTime, forChildren, stars);
    }

    private Integer findIssueNumber(Document modsDoc, String issuePid) {
        Node issueNumber = issueNumberXpath.selectSingleNode(modsDoc);
        if (issueNumber == null) {
            logger.log(Level.SEVERE, "Cannot find content of element mods:number for issue with pid ''{0}''", issuePid);
            return null;
        }
        String issueNumStr = issueNumber.getText();
        try {
            return Integer.valueOf(StringUtils.removeNonNumbers(issueNumStr));
        } catch (NumberFormatException e) {
            String[] params = {issueNumStr, issuePid};
            logger.log(Level.SEVERE, "Illegal value in element mods:number ''{0}'' for issue with pid ''{1}''", params);
            return null;
        }
    }

    private DayTime findIssueDayTime(Document modsDoc, String issuePid) {
        List<Node> noteList = issueDayTimeXpath.selectNodes(modsDoc);
        DayTime dayTime = extractDayTime(noteList);
        if (dayTime == null) {
            logger.log(Level.SEVERE, "Could not extract any dayTime value from mods:note elements for issue with PID ''{0}''", issuePid);
        }
        return dayTime;
    }

    private DayTime extractDayTime(List<Node> nodeList) {
        for (Node node : nodeList) {
            String text = node.getText();
            for (DayTime dt : DayTime.values()) {
                if (text.contains(dt.toString())) {
                    return dt;
                }
            }
        }
        return DayTime.unspecified;
    }

    private LocalDate findIssueDate(Document modsDoc, String issuePid) {
        Node dateNode = issueDateXpath.selectSingleNode(modsDoc);
        if (dateNode == null) {
            logger.log(Level.SEVERE, "Cannot find content of element mods:date for issue with pid ''{0}''", issuePid);
            return null;
        }
        String dateStr = dateNode.getText();
        LocalDate date = parseDate(dateStr);
        if (date == null) {
            String[] params = {dateStr, issuePid};
            logger.log(Level.SEVERE, "Illegal value in element mods:date ''{0}'' for issue with pid ''{1}''", params);
        }
        return date;
    }

    private LocalDate parseDate(String dateStr) {
        String[] parts = dateStr.split("\\.");
        if (parts.length != 3) {
            logger.log(Level.SEVERE, "incorrect format of date ''{0}''", dateStr);
            return null;
        }
        int day = Integer.valueOf(parts[0]);
        int month = Integer.valueOf(parts[1]);
        int year = Integer.valueOf(parts[2]);
        return new LocalDate(year, month, day);
    }

    private boolean isForChildren(Document modsDoc, String issuePid) {
        Node titleNode = issueTitleXpath.selectSingleNode(modsDoc);
        if (titleNode == null) {
            logger.log(Level.SEVERE, "Cannot find content of element mods:title for issue with pid ''{0}''", issuePid);
            return false;
        }
        String title = titleNode.getText();
        for (String pattern : FOR_CHILDREN_TITLE_STRINGS) {
            if (title.contains(pattern)) {
                return true;
            }
        }
        return false;
    }

    private int findStars(Document modsDoc, String issuePid) {
        Node numberNode = issueNumberXpath.selectSingleNode(modsDoc);
        if (numberNode == null) {
            logger.log(Level.SEVERE, "Cannot find content of element mods:number for issue with pid ''{0}''", issuePid);
            return 0;
        }
        return StringUtils.countCharacters(numberNode.getText(), '*');
    }

    private void addIssuePidMapping(Issue issue, String issuePid) {
        if (isDuplicate(issue)) {
            addDuplicatePid(issue, issuePid);
        } else {
            String oldPid = issuePidMap.get(issue);
            if (oldPid != null) {
                Object[] params = {issue.getNumber(), issue.getDayTime(), volumePid, oldPid, issuePid};
                logger.log(Level.SEVERE, "pid for issue (number {0}, {1}) and volume ''{2}'' already present. Found pid: ''{3}'', wanted to add pid: ''{4}''", params);
                issuePidMap.remove(issue);

                addDuplicatePid(issue, oldPid);
                addDuplicatePid(issue, issuePid);
            } else {
                issuePidMap.put(issue, issuePid);
            }
        }
    }

    private boolean isDuplicate(Issue issue) {
        return duplicateIssuePidMap.containsKey(issue);
    }

    private void addDuplicatePid(Issue issue, String issuePid) {
        //TODO: pristup k replicate pids. Nejlepe formou logu chyb ve fedore
        logger.log(Level.INFO, "adding pid ''{0}'' to list of duplicate pids for issue (number {1}, {2})", new Object[]{issuePid, issue.getNumber(), issue.getDayTime()});
        List<String> duplicatePids = duplicateIssuePidMap.get(issue);
        if (duplicatePids == null) {
            duplicatePids = new ArrayList<String>();
            duplicateIssuePidMap.put(issue, duplicatePids);
        }
        duplicatePids.add(issuePid);
    }

    private void addIssueDateMapping(Integer issueNumber, LocalDate date) {
        LocalDate oldDate = issueNumberDateMap.get(issueNumber);
        if (oldDate != null && !oldDate.equals(date)) {
            Object[] params = {issueNumber, volumePid, oldDate.toString(), date.toString()};
            logger.log(Level.SEVERE, "date for issue {0} and volume ''{1}'' present (from different day time) and the dates differ! Found date: ''{2}'', wanted to add date: ''{3}'' ", params);
        } else {
            issueNumberDateMap.put(issueNumber, date);
        }
    }

    @Override
    public String getVolumePid() {
        return volumePid;
    }

    /**
     *
     * @param issue
     * @return PID of the issue or null if there is not such an issue present
     */
//    @Override
//    public String getIssuePid(Issue issue) {
//        String issuePid = null;
//        for (Issue issueKey : issuePidMap.keySet()){
//            if (issue.myEquals(issueKey)){
//                issuePid = issuePidMap.get(issueKey);
//                issue.setStars(issueKey.getStars());
//                break;
//            }
//        }
//        if (issuePid == null) {
//            missingIssues.add(issue);
//        }
//        return issuePid;
//    }
    @Override
    public String getIssuePid(Issue issue) {
        String issuePid = issuePidMap.get(issue);
        if (issuePid == null) {
            missingIssues.add(issue);
        }
        return issuePid;
    }

    /**
     *
     * @param issueNumber
     * @return date of the issue or null if there is not such an issue present
     */
    @Override
    public LocalDate getIssueDate(Integer issueNumber) {
        //IssueNotDated issue = new IssueNotDated(issueNumber, dayTime);
        return issueNumberDateMap.get(issueNumber);
    }

    @Override
    public Set<Issue> getIssueSet() {
        return issuePidMap.keySet();
    }

    @Override
    public Map<Issue, List<String>> getDuplicateIssuePidMap() {
        return duplicateIssuePidMap;
    }

    @Override
    public Integer getVolumeNumber() {
        return volumeNumber;
    }

    @Override
    public Set<Issue> getMissingIssues() {
        return missingIssues;
    }
}
